/*
 *  Copyright (C) 2010 Glauco
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.balisunrise.bindings;

import org.balisunrise.common.Validator;
import java.util.ArrayList;
import java.util.List;
import org.balisunrise.common.BindingException;
import org.balisunrise.common.Converter;
import org.balisunrise.common.SyncResult;
import org.balisunrise.common.listeners.BindingListener;

/**
 * Construção basica para o Binding
 * Exemplo:<B>
 *     Uma entidade <code>MyClass</code> possui um atributo do tipo
 * <code>Date</code> com o nome <code>dataInicial</code>.<B>
 *     Então teriamos um <code>Binding<MyClass,Date,JFormattedTextField,String></code>
 * @author glacuo
 * @param <S> Source.
 * @param <P> Tipo.
 * @param <T> Target.
 * @param <D> Tipo.
 */
public class BeansBinding<S,P,T,D> implements Binding<S,P,T,D>{

    Converter<P, D> converter;

    Validator<P> validator;

    List<BindingListener> listeners = new ArrayList<BindingListener>();

    BindingSupport<P> sourceSupport;

    BindingSupport<D> targetSupport;

    BindingTrigger sourceTrigger;

    BindingTrigger targetTrigger;

    private S source;
    private T target;
    private String sourceProperty;
    private String targetProperty;
    private P oldSourcePropertyValue;
    private D oldTargetPropertyValue;

    private boolean locked = false;
    private SyncResult syncResult;

    public BeansBinding(S source, T target, String sourceProperty,
            String targetProperty, BindingSupport<P> sourceSupport,
            BindingSupport<D> targetSupport) {
        this.source = source;
        this.target = target;
        this.sourceProperty = sourceProperty;
        this.targetProperty = targetProperty;
        this.sourceSupport = sourceSupport;
        this.targetSupport = targetSupport;
    }

    @Override
    public S getSource(){
        return source;
    }

    @Override
    public T getTarget(){
        return target;
    }

    @Override
    public BindingSupport<P> getSourceSupport() {
        return sourceSupport;
    }

    @Override
    public void setSourceSupport(BindingSupport<P> sourceSupport) {
        this.sourceSupport = sourceSupport;
    }

    @Override
    public BindingTrigger getSourceTrigger() {
        return sourceTrigger;
    }

    @Override
    public void setSourceTrigger(BindingTrigger sourceTrigger) {
        this.sourceTrigger = sourceTrigger;
    }

    @Override
    public BindingSupport<D> getTargetSupport() {
        return targetSupport;
    }

    @Override
    public void setTargetSupport(BindingSupport<D> targetSupport) {
        this.targetSupport = targetSupport;
    }

    @Override
    public BindingTrigger getTargetTrigger() {
        return targetTrigger;
    }

    @Override
    public void setTargetTrigger(BindingTrigger targetTrigger) {
        this.targetTrigger = targetTrigger;
    }

    @Override
    public String getSourceProperty() {
        return sourceProperty;
    }

    @Override
    public String getTargetProperty() {
        return targetProperty;
    }

    public Validator<P> getValidator() {
        return validator;
    }

    @Override
    public BindingListener[] getListeners(BindingListener[] a) {
        return listeners.toArray(a);
    }

    @Override
    public BindingListener removeListener(int index) {
        return listeners.remove(index);
    }

    @Override
    public boolean removeListener(BindingListener o) {
        return listeners.remove(o);
    }

    @Override
    public void addListener(int index, BindingListener element) {
        listeners.add(index, element);
    }

    @Override
    public boolean addListener(BindingListener e) {
        return listeners.add(e);
    }

    @Override
    public void setConverter(Converter<P, D> converter) {
        this.converter = converter;
    }

    @Override
    public void setValidator(Validator<P> validator) {
        this.validator = validator;
    }

    @Override
    public final boolean sourcePropertyChanged(){

        if(!setLock(true))
            return true;

        SyncResult sync = syncSourceToTarget();

        fireSyncEvent(sync);

        boolean result;

        if (sync.getResultType().equals(SyncResult.ResultType.SUCCESS))
            result = true;
        else
            result = false;

        setLock(false);

        return result;
    }

    @Override
    public final boolean targerPropertyChanged() {

        if(!setLock(true))
            return true;

        SyncResult sync = syncTargetToSource();

        fireSyncEvent(sync);

        boolean result;

        if (sync.getResultType().equals(SyncResult.ResultType.SUCCESS))
            result = true;
        else
            result = false;

        setLock(false);

        return result;
    }

    @Override
    public void bind() {
        sourceTrigger.activate();
        targetTrigger.activate();
        sourcePropertyChanged();
        for (BindingListener bindingListener : listeners) {
            bindingListener.becameBound(this);
        }
    }

    @Override
    public void unBind() {
        sourceTrigger.deactivate();
        targetTrigger.deactivate();
        oldSourcePropertyValue = null;
        oldTargetPropertyValue = null;
        for (BindingListener bindingListener : listeners) {
            bindingListener.becameUnbound(this);
        }
    }

    @Override
    public SyncResult syncSourceToTarget() {

        SyncResult sync = getSyncResult();
        sync.setSyncType(SyncResult.SyncType.SOURCE_TO_TARGET);
        sync.setOldPropertyValue(oldSourcePropertyValue);

        P value = null;
        try{
            value = getSourcePropertyValue();
        }catch(BindingException ex){
            sync.setResultType(SyncResult.ResultType.FAILURE);
            sync.setCause(SyncResult.Cause.SOURCE_UNREADABLE);
            return sync;
        }

        oldSourcePropertyValue = value;
        sync.setNewPropertyValue(value);

        D o = null;
        try{
            o = convertToTarget(value);
        }catch(Exception ex){
            sync.setResultType(SyncResult.ResultType.EXCEPTION);
            sync.setCause(SyncResult.Cause.CONVERTION_EXCEPTION);
            sync.setExceptionMessage(ex.getMessage());
            return sync;
        }

        try{
            setTargetPropertyValue(o);
            oldTargetPropertyValue = o;
            sync.setResultType(SyncResult.ResultType.SUCCESS);
        }catch(BindingException ex){
            sync.setResultType(SyncResult.ResultType.FAILURE);
            sync.setCause(SyncResult.Cause.TARGET_UNWRITABLE);
            sync.setExceptionMessage(ex.getMessage());
            return sync;
        }catch(Exception ex){
            sync.setResultType(SyncResult.ResultType.EXCEPTION);
            sync.setCause(SyncResult.Cause.TARGET_EXCEPTION);
            sync.setExceptionMessage(ex.getMessage());
            return sync;
        }

        return sync;
    }

    @Override
    public SyncResult syncTargetToSource() {

        SyncResult sync = getSyncResult();
        sync.setSyncType(SyncResult.SyncType.TARGET_TO_SOURCE);
        sync.setOldPropertyValue(oldTargetPropertyValue);

        D value = null;
        try{
            value = getTargetPropertyValue();
        }catch(BindingException ex){
            sync.setResultType(SyncResult.ResultType.FAILURE);
            sync.setCause(SyncResult.Cause.TARGET_UNREADABLE);
            return sync;
        }

        sync.setNewPropertyValue(value);

        P o = null;
        try{
            o = convertToSource(value);
        }catch(Exception ex){
            sync.setResultType(SyncResult.ResultType.EXCEPTION);
            sync.setCause(SyncResult.Cause.CONVERTION_EXCEPTION);
            sync.setExceptionMessage(ex.getMessage());
            restoreTargetPropertyValue();
            return sync;
        }

        try{
            validate(o);
        }catch(Exception ex){
            sync.setResultType(SyncResult.ResultType.EXCEPTION);
            sync.setCause(SyncResult.Cause.VALIDATION_EXCEPTION);
            sync.setExceptionMessage(ex.getMessage());
            restoreTargetPropertyValue();
            return sync;
        }

        try{
            setSourcePropertyValue(o);
            oldTargetPropertyValue = value;
            sync.setResultType(SyncResult.ResultType.SUCCESS);
        }catch(BindingException ex){
            sync.setResultType(SyncResult.ResultType.FAILURE);
            sync.setCause(SyncResult.Cause.TARGET_UNWRITABLE);
            sync.setExceptionMessage(ex.getMessage());
            return sync;
        }catch(Exception ex){
            sync.setResultType(SyncResult.ResultType.EXCEPTION);
            sync.setCause(SyncResult.Cause.TARGET_EXCEPTION);
            sync.setExceptionMessage(ex.getMessage());
            restoreTargetPropertyValue();
            return sync;
        }

        return sync;
    }

    private synchronized boolean setLock(boolean lock){
        if(lock && locked)
            return false;

        locked = lock;
        return true;
    }

    private SyncResult getSyncResult(){
        if (syncResult == null){
            syncResult = new SyncResult();
            syncResult.setSource(source);
            syncResult.setSourceProperty(sourceProperty);
            syncResult.setTarget(target);
            syncResult.setTargetProperty(targetProperty);
        }
        syncResult.setCause(null);
        syncResult.setExceptionMessage(null);
        syncResult.setNewPropertyValue(null);
        syncResult.setOldPropertyValue(null);
        syncResult.setResultType(null);
        syncResult.setSyncType(null);
        return syncResult;
    }

    private void restoreTargetPropertyValue(){
        try {
            setTargetPropertyValue(oldTargetPropertyValue);
        } catch (Exception ex) {
            // se chegar aqui é problema.
            System.out.println(ex.getMessage());
        }
    }

    @Override
    public P getSourcePropertyValue() throws BindingException{

        P o;

        try {
            o = sourceSupport.getPropertyValue();
        } catch (BindingException ex) {
            throw ex;
        } catch (NullPointerException ex){
            o = null;
        } catch(Exception ex){
            throw new BindingException(
                    "Exception reading source property value", ex);
        }

        return o;
    }

    @Override
    public D getTargetPropertyValue() throws BindingException{

        D o;

        try{
            o = targetSupport.getPropertyValue();
        } catch (BindingException ex) {
            throw ex;
        } catch (NullPointerException ex){
            o = null;
        } catch(Exception ex){
            throw new BindingException(
                    "Exception reading target property value", ex);
        }

        return o;
    }

    @Override
    public P convertToSource(D value) throws Exception{

        if(converter != null)
            return converter.convertReverse(value);

        if(value != null)
            return (P) value;
        else
            return null;
    }

    @Override
    public D convertToTarget(P value) throws Exception{

        if(converter != null)
            return converter.convertForward(value);

        if (value != null)
            return (D) value;
        else
            return null;
    }

    @Override
    public void validate(P value) throws Exception{

        if(validator != null)
            validator.validate(value);
    }

    @Override
    public void setSourcePropertyValue(P value) throws BindingException,
            Exception{

        try{
            sourceSupport.setPropertyValue(value);
        } catch (BindingException ex) {
            throw ex;
        } catch (NullPointerException ex){
            throw new BindingException(
                    "Exception Writing source property value", ex);
        } catch(Exception ex){
            throw ex;
        }
    }

    @Override
    public void setTargetPropertyValue(D value) throws BindingException,
            Exception{

        try{
            targetSupport.setPropertyValue(value);
        } catch (BindingException ex) {
            throw ex;
        } catch (NullPointerException ex){
            throw new BindingException(
                    "Exception Writing target property value", ex);
        } catch(Exception ex){
            throw ex;
        }
    }

    protected void fireSyncEvent(SyncResult sync){
        for (BindingListener bindingListener : listeners) {
            bindingListener.synced(sync);
        }
    }
}
